/**
 * @Author: SevDaisy十七散人
 * @Date: 2020-11-17 14:59:28
 */
package zyy;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 业务
 */
enum Business {
  /* 存款 */
  DEPOSIT("    存款"),
  /* 取款 */
  WITH_DRAWAL("    取款"),
  /* 缴纳罚款 */
  PAY_FINE("缴纳罚款"),
  /* 开通网银 */
  OPEN_ONLINE_BANKING("开通网银"),
  /* 交水电费 */
  PAY_WATER_AND_ELECTRICITY_BILLS("交水电费"),
  /* 购买基金 */
  PURCHASE_FUND("购买基金"),
  /* 转账汇款 */
  TRANSFER_REMITTANCE("转账汇款"),
  /* 个贷还款 */
  INDIVIDUAL_LOAN_REPAYMENT("个贷还款");

  String msg_zhCN;

  private Business(String msg_zhCN) {
    this.msg_zhCN = msg_zhCN;
  }
}

/**
 * 顾客
 */
class Customer {

  String ID;/* 顾客的名字 */
  String type;/* 可选值 VIP normal */
  Business business;/* 业务 */
  String isRecepted;/* 可选值 waiting working over */
  long ms_arrived;/* 客户开始排队时的系统毫秒数 */
  long ms_start;/* 客户业务开始时的系统毫秒数 */
  long ms_over;/* 客户业务结束时的系统毫秒数 */

  private Customer() {
    super();
  }

  public static Customer come(String ID) {
    Customer out = new Customer();
    out.isRecepted = "waiting";
    out.ID = ID;
    return out;
  }

  public Customer setType(String type) {
    this.type = type;
    return this;
  }

  public Customer setBusiness(Business business) {
    this.business = business;
    return this;
  }

  public Customer setArrived() {
    this.ms_arrived = System.currentTimeMillis();
    return this;
  }

  @Override
  public String toString() {
    return ("Customer " + ID + " " + business.msg_zhCN + " " + type);
  }
}

/**
 * 窗口
 */
class Window {

  String ID;
  String isWorking;/* off wating working */
  String type;/* A B V */
  Map<Business, Boolean> ability;/* 不同类型的窗口能办理的业务列表 */
  Customer currentCustomer;
  boolean down_flag;

  public Window(String iD, String type) {
    this.ID = iD;
    this.isWorking = "off";
    this.currentCustomer = null;
    this.type = type;
    this.ability = new Hashtable<Business, Boolean>();
    this.down_flag = false;
    ability.put(Business.DEPOSIT, true);
    ability.put(Business.WITH_DRAWAL, true);
    ability.put(Business.PAY_FINE, true);
    ability.put(Business.OPEN_ONLINE_BANKING, true);
    ability.put(Business.PAY_WATER_AND_ELECTRICITY_BILLS, true);
    ability.put(Business.PURCHASE_FUND, true);
    ability.put(Business.TRANSFER_REMITTANCE, true);
    ability.put(Business.INDIVIDUAL_LOAN_REPAYMENT, true);
    if ("A".equals(type)) {} else if ("B".equals(type)) {
      ability.put(Business.PAY_FINE, false);
      ability.put(Business.PURCHASE_FUND, false);
      ability.put(Business.INDIVIDUAL_LOAN_REPAYMENT, false);
    } else if ("V".equals(type)) {}
  }

  void setDown() {
    this.down_flag = true;
    System.out.printf(
      "##### >开始结业清算< ###### %s 听说要下班了 当前状态:%s 当前用户:%s 当前时间: %tT\n",
      this.ID,
      this.isWorking,
      this.currentCustomer,
      FakeTime.fakeNow(System.currentTimeMillis())
    );
  }

  void set_ON(ScheduledExecutorService pool, List<Customer> works) {
    while (true) {
      /* 如果窗口未开 则打开此窗口 标记自身为等待模式 */
      if ("off".equals(this.isWorking)) {
        System.out.printf("%s 开始工作\n", this.ID);
        this.isWorking = "waiting";
      }
      /* 如果窗口正在等待工作 尝试得到新的当前客户 */
      if ("waiting".equals(this.isWorking)) {
        /* 获取当前大厅客户的锁 */
        synchronized (works) {
          /* 如果是VIP窗口，则要先扫描出有无VIP客户，先对VIP客户服务，所有业务类型均可 */
          if ("V".equals(this.type)) {
            /* 遍历当前大厅客户 */
            for (Customer who : works) {
              if ("VIP".equals(who.type)) {
                this.currentCustomer = works.remove(works.indexOf(who));
                break;
              }
            }
          }
          /**
           * 如果是非VIP窗口的话，那么所有客户都可以服务，只需检查业务类型
           * 如果VIP窗口没找到客户，那么也可以来这里再找一遍客户
           **/
          if (this.currentCustomer == null) {
            for (Customer who : works) {
              /* 如果发现某客户正在等待，且业务类型都符合要求 */
              if (
                "waiting".equals(who.isRecepted) &&
                this.ability.get(who.business)
              ) {
                this.currentCustomer = works.remove(works.indexOf(who));
                break;
              }
            }
          }
        }
        /* 如果抢占到了客户 */
        if (this.currentCustomer != null) {
          /* 标记自身为工作模式 */
          this.isWorking = "working";
          System.out.printf(
            "%s %s for %s at %s\n",
            this.ID,
            this.down_flag ? "DOWN" : this.isWorking,
            currentCustomer.ID,
            currentCustomer.business.msg_zhCN
          );
          /* 调用背景资源，对客户进行服务 */
          pool.schedule(
            new Thread(
              () -> {
                try {
                  Bank.do_business(this.currentCustomer);
                } catch (InterruptedException e) {
                  e.printStackTrace();
                }
              }
            ),
            0,
            TimeUnit.MILLISECONDS
          );
        }
      }
      /* 如果窗口正在工作 */
      if ("working".equals(this.isWorking)) {
        /* 检查当前客户活动是否已经完成 */
        if ("over".equals(this.currentCustomer.isRecepted)) {
          /* 是 则 标记自身为等待模式，并抛弃客户对象 */
          this.isWorking = "waiting";
          this.currentCustomer = null;
        }
      }
      /* 如果正处于收尾状态，并且客户列表已经空了 */
      if (down_flag) {
        if (this.currentCustomer == null && works.isEmpty()) {
          System.out.println(this.ID + " 我下班啦✧٩(ˊωˋ*)و✧");
          return;
        } else {}
      }
      /* 循环一次之后，无论如何，线程都应主动归还CPU */
      // Thread.yield();
      try {
        Thread.sleep(Bank.BASE_TIME);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

class LogItem {

  String msg;/* 为了方便打印而保存的字符串信息 */
  Customer cus;/* 每条日志的客户对象 */
  int order;/* 这是第几条日志 */

  public LogItem(Customer who) {
    this.cus = who;
  }

  public LogItem setOrder(int num) {
    this.msg =
      String.format(
        "%s: %s \t 到达时间:%s \t 办理业务类型:%s \t 总耗时:%.1f分钟",
        "VIP".equals(cus.type) ? "VIP " : "普通",
        cus.ID,
        Bank.sdf.format(FakeTime.fakeNow(cus.ms_arrived)),
        cus.business.msg_zhCN,
        FakeTime.fakeDurtion(cus.ms_over - cus.ms_start) / 60
      );
    this.order = num + 1;
    return this;
  }

  @Override
  public String toString() {
    return "LogItem " + order + " " + msg;
  }
}

class FakeTime {

  static long real_start;/* 真的开门时间 */
  static long fake_start;/* 假的开门时间 */
  static long fake_end;/* 假的关门时间 */
  static final long rate = 30 * 1000;/* 真假时间的比例 1毫秒 : 30秒 */

  /**
   * @param begin 真实的银行启动时间
   * @param work_start 几点上班
   * @param work_over 几点下班
   */
  static void init(Date begin, int work_start, int work_over) {
    FakeTime.real_start = begin.getTime();
    GregorianCalendar gnow = new GregorianCalendar();
    gnow.setTime(begin);
    gnow.set(GregorianCalendar.MINUTE, 0);
    gnow.set(GregorianCalendar.SECOND, 0);
    gnow.set(GregorianCalendar.MILLISECOND, 0);
    gnow.set(GregorianCalendar.HOUR_OF_DAY, work_start);
    FakeTime.fake_start = gnow.getTimeInMillis();
    gnow.set(GregorianCalendar.HOUR_OF_DAY, work_over);
    FakeTime.fake_end = gnow.getTimeInMillis();
  }

  static Date fakeNow(long millisecond_now) {
    return new Date(fake_start + (millisecond_now - real_start) * rate);
  }

  static double fakeDurtion(long diff) {
    return rate * diff / 1000.0;
  }
}

/**
 * 银行
 */
public class Bank {

  /* 基准时间 */
  static final long BASE_TIME = 1;
  /* Bank 中的静态线程池实例，用作此Java工程的背景资源 */
  static ScheduledExecutorService scheduled_pool = Executors.newScheduledThreadPool(
    20
  );
  /* 窗口其实是一层映射逻辑，从 客户发起请求 到 占用线程池 的映射 */
  static List<Window> windows = new ArrayList<Window>(
    Arrays.asList(
      new Window[] {
        new Window("A_1", "A"),
        new Window("B_2", "B"),
        new Window("B_1", "B"),
        new Window("V_1", "V"),
      }
    )
  );
  /* 客户统计日志 */
  static ArrayList<LogItem> log = new ArrayList<LogItem>(100);
  /* 时间格式器 */
  static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  /* 开始营业时间 */
  static Date bank_start = new Date();
  /* 预设营业时长 */
  // static long durtion = 1000 * 2;
  /* 银行关门标志 */
  static boolean off_flag = false;
  /* 设置新客频率 低频～高频 */
  static int nc_low = 2;
  static int nc_high = 4;

  static void do_business(Customer who) throws InterruptedException {
    Random random = new Random();
    /* System.out.printf("%s doing %s\n", who.ID, who.business); */
    who.isRecepted = "working";
    who.ms_start = System.currentTimeMillis();
    switch (who.business) {
      case DEPOSIT:
        Thread.sleep(BASE_TIME * (random.nextInt(15) + 5) / 10);
        break;
      case WITH_DRAWAL:
        Thread.sleep(BASE_TIME * (random.nextInt(15) + 5) / 10);
        break;
      case PAY_FINE:
        Thread.sleep(BASE_TIME * (random.nextInt(20) + 12) / 10);
        break;
      case OPEN_ONLINE_BANKING:
        Thread.sleep(BASE_TIME * (random.nextInt(80) + 50) / 10);
        break;
      case PAY_WATER_AND_ELECTRICITY_BILLS:
        Thread.sleep(BASE_TIME * (random.nextInt(20) + 15) / 10);
        break;
      case PURCHASE_FUND:
        Thread.sleep(BASE_TIME * (random.nextInt(30) + 20) / 10);
        break;
      case TRANSFER_REMITTANCE:
        Thread.sleep(BASE_TIME * (random.nextInt(40) + 30) / 10);
        break;
      case INDIVIDUAL_LOAN_REPAYMENT:
        Thread.sleep(BASE_TIME * (random.nextInt(40) + 20) / 10);
        break;
      default:
        throw new RuntimeException("用户的Business类型不是合法值");
    }
    /* System.out.printf("%s Over %s\n", who.ID, who.business); */
    who.isRecepted = "over";
    who.ms_over = System.currentTimeMillis();
    /* 记录客户统计日志 */
    synchronized (Bank.log) {
      Bank.log.add(new LogItem(who).setOrder(Bank.log.size()));
    }
  }

  /* 网上找的随机字符串方法 */
  public static String getRandomString(int length) {
    String str =
      "刘一陈二张三李四王五赵六孙七周八吴九郑十甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥";
    Random random = new Random();
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < length; i++) {
      int number = random.nextInt(42);
      sb.append(str.charAt(number));
    }
    return sb.toString();
  }

  /* 网上找的随机枚举方法 后来发现可以不用 */
  public static <T extends Enum<?>> T getRandomEnum(Class<T> clazz) {
    Random random = new Random();
    int x = random.nextInt(clazz.getEnumConstants().length);
    return clazz.getEnumConstants()[x];
  }

  /**
   * @param len      生成用户列表的长度,不应该小于20
   * @param mode     用户中各种业务的比例【 A | B | else 】
   * @param vip_rate 用户中，vip用户所占的百分比
   * @return 长度为 {@code len} 的 {@code List<Customer>}({@code ArrayList<Customer>})
   */
  public static List<Customer> init_customer_store(
    int len,
    char mode,
    double vip_rate
  ) {
    List<Business> business_store = new ArrayList<Business>(
      len
    );/* 受限制的随机业务容器 */
    List<Boolean> isVip_store = new ArrayList<Boolean>(
      len
    );/* 受限制的随机Vip资格容器 */
    Map<Business, Double> business_rate = new Hashtable<Business, Double>();/* 按 mode 为业务的随机性赋限制的值 */
    if (mode == 'A') {
      business_rate.put(Business.DEPOSIT, 0.2);
      business_rate.put(Business.WITH_DRAWAL, 0.2);
      business_rate.put(Business.PAY_FINE, 0.1);
      business_rate.put(Business.OPEN_ONLINE_BANKING, 0.1);
      business_rate.put(Business.PAY_WATER_AND_ELECTRICITY_BILLS, 0.05);
      business_rate.put(Business.PURCHASE_FUND, 0.15);
      business_rate.put(Business.TRANSFER_REMITTANCE, 0.1);
      business_rate.put(Business.INDIVIDUAL_LOAN_REPAYMENT, 0.1);
    } else if (mode == 'B') {
      business_rate.put(Business.DEPOSIT, 0.1);
      business_rate.put(Business.WITH_DRAWAL, 0.1);
      business_rate.put(Business.PAY_FINE, 0.05);
      business_rate.put(Business.OPEN_ONLINE_BANKING, 0.05);
      business_rate.put(Business.PAY_WATER_AND_ELECTRICITY_BILLS, 0.05);
      business_rate.put(Business.PURCHASE_FUND, 0.4);
      business_rate.put(Business.TRANSFER_REMITTANCE, 0.05);
      business_rate.put(Business.INDIVIDUAL_LOAN_REPAYMENT, 0.2);
    } else {
      business_rate.put(Business.DEPOSIT, 1.0);
      business_rate.put(Business.WITH_DRAWAL, 0.0);
      business_rate.put(Business.PAY_FINE, 0.0);
      business_rate.put(Business.OPEN_ONLINE_BANKING, 0.0);
      business_rate.put(Business.PAY_WATER_AND_ELECTRICITY_BILLS, 0.0);
      business_rate.put(Business.PURCHASE_FUND, 0.0);
      business_rate.put(Business.TRANSFER_REMITTANCE, 0.0);
      business_rate.put(Business.INDIVIDUAL_LOAN_REPAYMENT, 0.0);
    }

    /* 初步生成符合要求业务集合，业务比例按模式要求 */
    business_rate.forEach(
      (k, v) -> {
        /* 对具体的数字向下取整 */
        int size = (int) (v * len);
        for (int i = 0; i < size; i++) {
          business_store.add(k);
        }
      }
    );
    /* 打乱有序的业务集合，使其随机化 */
    Collections.shuffle(business_store);
    /* 因为之前的向下取整，所以业务数量可能会达不到len的要求，进行补齐 */
    if (business_store.size() < len) {
      for (int i = business_store.size(), j = 0; i < len; i++, j++) {
        // System.out.printf("len: %d \t now_size: %d\n", len, business_store.size());
        // System.out.println("add : " + business_store.get(j));
        business_store.add(business_store.get(j));
      }
      /* 尾部补充的和头部的顺序是一样的，再打乱一次 */
      Collections.shuffle(business_store);
    }

    /* 按Vip率来生成一定数量的 Vip 资格 */
    for (int i = 0; i < (int) (vip_rate * len); i++) {
      isVip_store.add(true);
    }
    /* 数量不足len的部分，用 非Vip 资格去填充补齐 */
    for (int i = isVip_store.size(); i < len; i++) {
      isVip_store.add(false);
    }
    /* 打乱有序的Vip资格表，使其随机化 */
    Collections.shuffle(isVip_store);

    /* 构建长度为len的随机用户表 */
    List<Customer> out = new ArrayList<Customer>(len);

    /* 对外返回生成好的客户列表 */
    for (int i = 0; i < len; i++) {
      out.add(
        Customer
          .come(getRandomString(3))
          .setBusiness(business_store.get(i))
          .setType(isVip_store.get(i) ? "VIP" : "normal")
      );
    }
    return out;
  }

  public static void analyise_log(ArrayList<LogItem> logs) {
    double time_sum = 0;
    HashMap<Business, Double> cntMap = new HashMap<>();
    cntMap.put(Business.DEPOSIT, 0.0);
    cntMap.put(Business.WITH_DRAWAL, 0.0);
    cntMap.put(Business.PAY_FINE, 0.0);
    cntMap.put(Business.OPEN_ONLINE_BANKING, 0.0);
    cntMap.put(Business.PAY_WATER_AND_ELECTRICITY_BILLS, 0.0);
    cntMap.put(Business.PURCHASE_FUND, 0.0);
    cntMap.put(Business.TRANSFER_REMITTANCE, 0.0);
    cntMap.put(Business.INDIVIDUAL_LOAN_REPAYMENT, 0.0);
    for (LogItem log : logs) {
      time_sum += FakeTime.fakeDurtion(log.cus.ms_over - log.cus.ms_arrived);
      cntMap.put(log.cus.business, cntMap.get(log.cus.business) + 1);
    }
    double time_ave = time_sum / logs.size();
    /* 开始打印 */
    logs.forEach(s -> System.out.println(s));
    System.out.printf(
      "\n\n所有用户的平均办理时间是: %.3f分钟\n",
      time_ave / 60
    );
    System.out.println("各业务的比例是");
    for (Map.Entry<Business, Double> entry : cntMap.entrySet()) {
      System.out.printf(
        "\t%s:\t%.2f%%\n",
        entry.getKey().msg_zhCN,
        100 * entry.getValue() / logs.size()
      );
    }
  }

  public static void main(String[] args) {
    /* 输出基准时间的信息 */
    System.out.printf("基准时间是 %.3f 秒\n", Bank.BASE_TIME / 1000.0);
    /* 虚拟时间初始化 */
    FakeTime.init(Bank.bank_start, 8, 17);
    /* 建立客户伪随机库 —— 还没到银行但是会到银行的用户列表 */
    List<Customer> customer_outside = new ArrayList<Customer>();
    char cus_mode = 'A';
    double cus_vip_rate = 0.2;
    /* 建立客户收留容器 —— 已经到银行摇号排队的用户列表 */
    List<Customer> customer_inside = new ArrayList<Customer>();

    /* 随机添加客户 */
    scheduled_pool.scheduleAtFixedRate(
      new Thread(
        () -> {
          /* 如果银行已经关闭了，则新客进程关闭 */
          if (Bank.off_flag) return;
          Customer nextCustomer;
          synchronized (customer_outside) {
            if (customer_outside.isEmpty()) {
              /* debug: 此处不能用 customer_outside=..., 只能用 addAll */
              customer_outside.addAll(
                init_customer_store(100, cus_mode, cus_vip_rate)
              );
              /* System.out.println("Size is " + customer_outside.size()); */
            }
            /* debug: size 如果没有 -1 就直接去 get 会导致下标越界，程序会终止但是不会抛出报错 */
            nextCustomer = customer_outside.get(customer_outside.size() - 1);
            customer_outside.remove(nextCustomer);
          }
          System.out.printf(
            "\t\t\t\t\t\t\t\t新客户 %s\n",
            nextCustomer.toString()
          );
          synchronized (customer_inside) {
            customer_inside.add(nextCustomer.setArrived());
          }
          try {
            /* 至多 nc_high 个基准时间 */
            Thread.sleep(
              (int) (
                Bank.nc_high * Bank.BASE_TIME * (new Random().nextDouble())
              )
            );
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      ),
      0,
      /* 线程间隔至少 nc_low 个基准时间 */
      Bank.nc_low * Bank.BASE_TIME,
      TimeUnit.MILLISECONDS
    );

    /* 启动窗口 */
    windows.forEach(
      window ->
        scheduled_pool.schedule(
          new Thread(
            () -> {
              window.set_ON(scheduled_pool, customer_inside);
            }
          ),
          3 * Bank.BASE_TIME,
          TimeUnit.MILLISECONDS
        )
    );

    /* 关门监听线程 */
    scheduled_pool.scheduleAtFixedRate(
      new Thread(
        () -> {
          /* 如果模拟时间已经到了下班时间，则进入结业结算 */
          if (
            FakeTime
              .fakeNow(System.currentTimeMillis())
              .after(new Date(FakeTime.fake_end))
          ) {
            if (Bank.off_flag == false) {
              /* 设置银行关门标志为真，银行进入结业结算 */
              Bank.off_flag = true;
              /* 遍历窗口列表，将每个窗口实例设置为下班模式，触发窗口的自主终止 */
              windows.forEach(win -> win.setDown());
            } else {
              synchronized (customer_inside) {
                /* 若已经没有在排队的顾客 则 打印结束日志 */
                if (customer_inside.isEmpty()) {
                  synchronized (Bank.log) {
                    Bank.analyise_log(Bank.log);
                    System.out.printf(
                      "程序持续运行了 %.3f 秒\n",
                      (System.currentTimeMillis() - Bank.bank_start.getTime()) /
                      1000.0
                    );
                    /* 发送终止请求 线程池中全体预备退出 */
                    scheduled_pool.shutdown();
                    return;
                  }
                }
              }
            }
          } else {
            try {
              Thread.sleep(Bank.BASE_TIME);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }
      ),
      0,
      10,
      TimeUnit.MILLISECONDS
    );
  }
}
